- How To Handle
XCTAssert*in Helper Methods? - How To Detect Memory Leaks in Unit Tests?
- How To Test That Function Throws An Error?
- How To Test That Function Does Not Throw An Error?
- How To Test Optional Values?
- How To Check That a Callback is Not Called?
- How To Test Asynchronous Callbacks?
- How To Test
viewDidLoadmethod fromUIViewController? - References
When calling the XCTAssert* functions outside the test method (e.g., in helper methods), it's crucial to provide the #filePath and #line parameters.
This allows Xcode to precisely identify the specific test that failed and its location (the actual tested method, not a helper method).
func test_method() {
// ... Arrange & Act
// Assert: helper method
helperExpect(param) // It should fail here ✅
}
private func helperExpect(param: Bool, file: StaticString = #filePath, line: UInt = #line) {
//...
XCTAssertTrue(param: Bool, file: file, line: line) // Not here ❌
}If you're testing the collaboration between two classes in a parent-child relationship (for example, where the parent is the SUT and the child is a Spy), there's a risk of having a retain cycle, especially when asynchronous functions with closures are involved. To verify the presence of a retain cycle and potential memory leaks, you can follow these steps:
// A factory helper for SUT creation
private func makeSUT(file: StaticString = #filePath, line: UInt = #line) -> SomeSutType {
// Instantiate the Spy
let spy = ...
// Instantiate the SUT
let sut = ...
assertForMemoryLeakOnTeardown(spy, file: file, line: line) // ✅ Check the Spy instance for memory leaks
assertForMemoryLeakOnTeardown(sut, file: file, line: line) // ✅ Check the SUT instance for memory leaks
return sut
}
private func assertForMemoryLeakOnTeardown(_ object: AnyObject, file: StaticString = #filePath, line: UInt = #line) {
// ✅ This block runs assertion when a test is finished
addTeardownBlock { [weak object] in
XCAssertNil(object, "The object instance has not been deallocated.", file: file, line: line)
}
}This method adds branching logic (do-catch) to the test which is not desired.
func test_validate_whenPhoneNumberIsInvalid_shouldThrowException() {
let sut = PhoneNumberValidator()
let INVALID_NUMBER = "g122345j"
do {
try sut.validate(INVALID_NUMBER)
XCTFail("The validate() was supposed to throw an error.")
} catch PhoneNumberValidator.Error {
// Successfully passing
return
} catch {
XCTFail("The validate() was supposed to throw `PhoneNumberValidator.Error` when phone number is invalid. A different error was thrown.")
}
}You can also avoid do-catch by using XCTAssertThrowsError.
func test_validate_whenPhoneNumberIsInvalid_shouldThrowException() {
let sut = PhoneNumberValidator()
let INVALID_NUMBER = "g122345j"
let act = {
try sut.validate(INVALID_NUMBER)
}
XCTAssertThrowsError(try act(), "Invalid number error should be thrown") { error in // ✅
XCTAssertEqual(error as? PhoneNumberValidator.Error, .invalidNumber)
}
}func test_validate_whenPhoneNumberIsValid_shouldNotThrowException() {
let sut = PhoneNumberValidator()
let VALID_NUMBER = "777-777-777"
do {
let result = try sut.validate(VALID_NUMBER)
XCTAssertTrue(result)
} catch {
XCTFail("The validate() was not supposed to throw an error.")
}
}Mark XCTestCase methods as throwable to avoid do-catch in your test code.
func test_validate_whenPhoneNumberIsValid_shouldNothrowException() {
let sut = PhoneNumberValidator()
let VALID_NUMBER = "777-777-777"
let result = try sut.validate(VALID_NUMBER)
XCTAssertTrue(result)
}func test_validate_whenPhoneNumberIsValid_shouldNotThrowException() {
let sut = PhoneNumberValidator()
let VALID_NUMBER = "777-777-777"
XCTAssertNoThrow(try sut.validate(VALID_NUMBER), "The validate() should not throw an error when the phone number is valid")
}Use try XCTUnwrap() to test optional values
func testOptional() throws {
let optionalValue: Int? = 11
XCTAssertNotNil(unwrappedValue, optionalValue!) // ❌
}
func testOptional() throws {
let optionalValue: Int? = 11
let unwrappedValue = try XCTUnwrap(optionalValue) // ✅
XCTAssertEqual(unwrappedValue, 11)
}
func test_viewDidLoad_loadsConsentScript() throws {
// Given `SomeViewController()` which might return `nil`
let sut = try XCTUnwrap(SomeViewController())
// When
sut.loadViewIfNeeded()
/* ... */
}Use expectation.isInverted for to check that a callback is not called:
func test_observeQueueBecomingEmpty_whenDequeuedCalledAndQueueIsStillNotEmpty_shouldNotCallObservingHandler() {
let sut = QueueService(queue: ["George", "Sam", "Steven"])
let expectation = expectation(description: "Handler for the queue becoming empty")
expectation.isInverted = true // ✅
sut.observeQueueBecomingEmpty {
XCTFail("The observation handler for the queue becoming empty should not be triggered")
expectation.fulfill()
}
sut.dequeue()
waitForExpectations(timeout: 0.1)
}func test_fetch_shouldGetBooks() {
let sut = BookRepository()
// Create an expectation ✅
let expectation = expectation(description: "Loading books")
sut.fetch {
// Mark the expectation as fulfilled ✅
expectation.fulfill()
}
// Wait for all expectations to be fulfilled ✅
waitForExpectations(timeout: 1)
XCTAssertFalse(sut.books.isEmpty)
}Do not call the viewDidLoad method directly, as it might be called multiple times. Instead, call loadViewIfNeeded to invoke viewDidLoad indirectly.
func test_viewDidLoad() {
// Given
let sut = SomeViewController()
// When
sut.loadViewIfNeeded()
// Then
/* ... */
}